/*
 *  ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
 *  Copyright (C) 2012 Kristian S. Stangeland
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the
 *  GNU General Public License as published by the Free Software Foundation; either version 2 of
 *  the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with this program;
 *  if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 *  02111-1307 USA
 */

package com.comphenix.protocol.async;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;

import com.comphenix.protocol.PacketStream;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLogger;
import com.comphenix.protocol.events.NetworkMarker;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.PrioritizedListener;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.google.common.primitives.Longs;

/**
 * Contains information about the packet that is being processed by asynchronous listeners.
 * <p>
 * Asynchronous listeners can use this to set packet timeout or transmission order.
 * 
 * @author Kristian
 */
public class AsyncMarker implements Serializable, Comparable<AsyncMarker> {
		
	/**
	 * Generated by Eclipse.
	 */
	private static final long serialVersionUID = -2621498096616187384L;

	/**
	 * Default number of milliseconds until a packet will rejected.
	 */
	public static final int DEFAULT_TIMEOUT_DELTA = 1800 * 1000;
	
	/**
	 * Default number of packets to skip.
	 */
	public static final int DEFAULT_SENDING_DELTA = 0;
	
	/**
	 * The packet stream responsible for transmitting the packet when it's done processing.
	 */
	private transient PacketStream packetStream;
	
	/**
	 * Current list of async packet listeners.
	 */
	private transient Iterator<PrioritizedListener<AsyncListenerHandler>> listenerTraversal;
	
	// Timeout handling
	private long initialTime;
	private long timeout;
	
	// Packet order
	private long originalSendingIndex;
	private long newSendingIndex;
	
	// Used to determine if a packet must be reordered in the sending queue
	private Long queuedSendingIndex;
	
	// Whether or not the packet has been processed by the listeners
	private volatile boolean processed;
	
	// Whether or not the packet has been sent
	private volatile boolean transmitted;
	
	// Whether or not the asynchronous processing itself should be cancelled
	private volatile boolean asyncCancelled;
	
		// Whether or not to delay processing
	private AtomicInteger processingDelay = new AtomicInteger();
	
	// Used to synchronize processing on the shared PacketEvent
	private Object processingLock = new Object();
	
	// Used to identify the asynchronous worker
	private transient AsyncListenerHandler listenerHandler;
	private transient int workerID;
	
	// Determine if Minecraft processes this packet asynchronously
	private volatile static Method isMinecraftAsync;
	private volatile static boolean alwaysSync;

	/**
	 * Create a container for asyncronous packets.
	 * @param initialTime - the current time in milliseconds since 01.01.1970 00:00.
	 */
	AsyncMarker(PacketStream packetStream, long sendingIndex, long initialTime, long timeoutDelta) {
		if (packetStream == null)
			throw new IllegalArgumentException("packetStream cannot be NULL");
		
		this.packetStream = packetStream;
		
		// Timeout
		this.initialTime = initialTime;
		this.timeout = initialTime + timeoutDelta;
		
		// Sending index
		this.originalSendingIndex = sendingIndex;
		this.newSendingIndex = sendingIndex;
	}
	
	/**
	 * Retrieve the time the packet was initially queued for asynchronous processing.
	 * @return The initial time in number of milliseconds since 01.01.1970 00:00.
	 */
	public long getInitialTime() {
		return initialTime;
	}

	/**
	 * Retrieve the time the packet will be forcefully rejected.
	 * @return The time to reject the packet, in milliseconds since 01.01.1970 00:00.
	 */
	public long getTimeout() {
		return timeout;
	}
	
	/**
	 * Set the time the packet will be forcefully rejected.
	 * @param timeout - time to reject the packet, in milliseconds since 01.01.1970 00:00.
	 */
	public void setTimeout(long timeout) {
		this.timeout = timeout;
	}

	/**
	 * Retrieve the order the packet was originally transmitted.
	 * @return The original packet index.
	 */
	public long getOriginalSendingIndex() {
		return originalSendingIndex;
	}

	/**
	 * Retrieve the desired sending order after processing has completed.
	 * <p>
	 * Higher sending order means lower priority.
	 * @return Desired sending order.
	 */
	public long getNewSendingIndex() {
		return newSendingIndex;
	}

	/**
	 * Sets the desired sending order after processing has completed.
	 * <p>
	 * Higher sending order means lower priority.
	 * @param newSendingIndex - new packet send index.
	 */
	public void setNewSendingIndex(long newSendingIndex) {
		this.newSendingIndex = newSendingIndex;
	}

	/**
	 * Retrieve the packet stream responsible for transmitting this packet.
	 * @return The packet stream.
	 */
	public PacketStream getPacketStream() {
		return packetStream;
	}

	/**
	 * Sets the output packet stream responsible for transmitting this packet.
	 * @param packetStream - new output packet stream.
	 */
	public void setPacketStream(PacketStream packetStream) {
		this.packetStream = packetStream;
	}

	/**
	 * Retrieve whether or not this packet has been processed by the async listeners.
	 * @return TRUE if it has been processed, FALSE otherwise.
	 */
	public boolean isProcessed() {
		return processed;
	}

	/**
	 * Sets whether or not this packet has been processed by the async listeners.
	 * @param processed - TRUE if it has, FALSE otherwise.
	 */
	void setProcessed(boolean processed) {
		this.processed = processed;
	}

	/**
	 * Increment the number of times the current packet must be signalled as done before its transmitted.
	 * <p>
	 * This is useful if an asynchronous listener is waiting for further information before the
	 * packet can be sent to the user. A packet listener <b>MUST</b> eventually call
	 * {@link AsyncFilterManager#signalPacketTransmission(PacketEvent)},
	 * even if the packet is cancelled, after this method is called.
	 * <p>
	 * It is recommended that processing outside a packet listener is wrapped in a synchronized block
	 * using the {@link #getProcessingLock()} method.
	 * 
	 * @return The new processing delay.
	 */
	public int incrementProcessingDelay() {
		return processingDelay.incrementAndGet();
	}
	
	/**
	 * Decrement the number of times this packet must be signalled as done before it's transmitted.
	 * @return The new processing delay. If zero, the packet should be sent.
	 */
	int decrementProcessingDelay() {
		return processingDelay.decrementAndGet();
	}
	
	/**
	 * Retrieve the number of times a packet must be signalled to be done before it's sent.
	 * @return Number of processing delays.
	 */
	public int getProcessingDelay() {
		return processingDelay.get();
	}
	
	/**
	 * Whether or not this packet is or has been queued for processing.
	 * @return TRUE if it has, FALSE otherwise.
	 */
	public boolean isQueued() {
		return queuedSendingIndex != null;
	}

	/**
	 * Retrieve the sending index when the packet was queued.
	 * @return Queued sending index.
	 */
	public long getQueuedSendingIndex() {
		return queuedSendingIndex != null ? queuedSendingIndex : 0;
	}

	/**
	 * Set the sending index when the packet was queued.
	 * @param queuedSendingIndex - sending index.
	 */
	void setQueuedSendingIndex(Long queuedSendingIndex) {
		this.queuedSendingIndex = queuedSendingIndex;
	}

	/**
	 * Processing lock used to synchronize access to the parent PacketEvent and PacketContainer.
	 * <p>
	 * This lock is automatically acquired for every asynchronous packet listener. It should only be
	 * used to synchronize access to a PacketEvent if it's processing has been delayed.
	 * @return A processing lock.
	 */
	public Object getProcessingLock() {
		return processingLock;
	}

	public void setProcessingLock(Object processingLock) {
		this.processingLock = processingLock;
	}

	/**
	 * Retrieve whether or not this packet has already been sent.
	 * @return TRUE if it has been sent before, FALSE otherwise.
	 */
	public boolean isTransmitted() {
		return transmitted;
	}

	/**
	 * Determine if this packet has expired.
	 * @return TRUE if it has, FALSE otherwise.
	 */
	public boolean hasExpired() {
		return hasExpired(System.currentTimeMillis());
	}
	
	/**
	 * Determine if this packet has expired given this time.
	 * @param currentTime - the current time in milliseconds since 01.01.1970 00:00.
	 * @return TRUE if it has, FALSE otherwise.
	 */
	public boolean hasExpired(long currentTime) {
		return timeout < currentTime;
	}
	
	/**
	 * Determine if the asynchronous handling should be cancelled.
	 * @return TRUE if it should, FALSE otherwise.
	 */
	public boolean isAsyncCancelled() {
		return asyncCancelled;
	}

	/**
	 * Set whether or not the asynchronous handling should be cancelled.
	 * <p>
	 * This is only relevant during the synchronous processing. Asynchronous
	 * listeners should use the normal cancel-field to cancel a PacketEvent.
	 * 
	 * @param asyncCancelled - TRUE to cancel it, FALSE otherwise.
	 */
	public void setAsyncCancelled(boolean asyncCancelled) {
		this.asyncCancelled = asyncCancelled;
	}

	/**
	 * Retrieve the current asynchronous listener handler.
	 * @return Asychronous listener handler, or NULL if this packet is not asynchronous.
	 */
	public AsyncListenerHandler getListenerHandler() {
		return listenerHandler;
	}

	/**
	 * Set the current asynchronous listener handler.
	 * <p>
	 * Used by the worker to update the value.
	 * @param listenerHandler - new listener handler.
	 */
	void setListenerHandler(AsyncListenerHandler listenerHandler) {
		this.listenerHandler = listenerHandler;
	}

	/**
	 * Retrieve the current worker ID.
	 * @return Current worker ID.
	 */
	public int getWorkerID() {
		return workerID;
	}

	/**
	 * Set the current worker ID.
	 * <p>
	 * Used by the worker.
	 * @param workerID - new worker ID.
	 */
	void setWorkerID(int workerID) {
		this.workerID = workerID;
	}

	/**
	 * Retrieve iterator for the next listener in line.
	 * @return Next async packet listener iterator.
	 */
	Iterator<PrioritizedListener<AsyncListenerHandler>> getListenerTraversal() {
		return listenerTraversal;
	}
	
	/**
	 * Set the iterator for the next listener.
	 * @param listenerTraversal - the new async packet listener iterator.
	 */
	void setListenerTraversal(Iterator<PrioritizedListener<AsyncListenerHandler>> listenerTraversal) {
		this.listenerTraversal = listenerTraversal;
	}
	
	/**
	 * Transmit a given packet to the current packet stream.
	 * @param event - the packet to send.
	 * @throws IOException If the packet couldn't be sent.
	 */
	void sendPacket(PacketEvent event) throws IOException {
		try {
			if (event.isServerPacket()) {
				packetStream.sendServerPacket(event.getPlayer(), event.getPacket(), NetworkMarker.getNetworkMarker(event), false);
			} else {
				packetStream.recieveClientPacket(event.getPlayer(), event.getPacket(), NetworkMarker.getNetworkMarker(event), false);
			}
			transmitted = true;
			
		} catch (InvocationTargetException e) {
			throw new IOException("Cannot send packet", e);
		} catch (IllegalAccessException e) {
			throw new IOException("Cannot send packet", e);
		}
	}
	
	/**
	 * Determine if Minecraft allows asynchronous processing of this packet.
	 * @param event - packet event
	 * @return TRUE if it does, FALSE otherwise.
	 * @throws FieldAccessException If determining fails for some reasaon
	 */
	public boolean isMinecraftAsync(PacketEvent event) throws FieldAccessException {
		if (isMinecraftAsync == null && !alwaysSync) {
			try {
				isMinecraftAsync = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()).getMethodByName("a_.*");
			} catch (RuntimeException e) {
				// This will occur in 1.2.5 (or possibly in later versions)
				List<Method> methods = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()).
										getMethodListByParameters(boolean.class, new Class[] {});
				
				// Try to look for boolean methods
				if (methods.size() == 2) {
					isMinecraftAsync = methods.get(1);
				} else if (methods.size() == 1) {
					// We're in 1.2.5
					alwaysSync = true;
				} else if (MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.BOUNTIFUL_UPDATE)) {
					// The centralized async marker was removed in 1.8
					// Incoming chat packets can be async
					if (event.getPacketType() == PacketType.Play.Client.CHAT) {
						String content = event.getPacket().getStrings().readSafely(0);
						if (content != null) {
							// Incoming chat packets are async only if they aren't commands
							return ! content.startsWith("/");
						} else {
							ProtocolLogger.log(Level.WARNING, "Failed to determine contents of incoming chat packet!");
							alwaysSync = true;
						}
					} else if (event.getPacketType() == PacketType.Status.Server.SERVER_INFO) {
						return true;
					} else {
						// TODO: Find more cases of async packets
						return false;
					}
				} else {
					ProtocolLogger.log(Level.INFO, "Could not determine asynchronous state of packets (this can probably be ignored)");
					alwaysSync = true;
				}
			}
		}

		if (alwaysSync) {
			return false;
		} else {
			try {
				// Wrap exceptions
				return (Boolean) isMinecraftAsync.invoke(event.getPacket().getHandle());
			} catch (IllegalArgumentException e) {
				throw new FieldAccessException("Illegal argument", e);
			} catch (IllegalAccessException e) {
				throw new FieldAccessException("Unable to reflect method call 'a_', or: isAsyncPacket.", e);
			} catch (InvocationTargetException e) {
				throw new FieldAccessException("Minecraft error", e);
			}
		}
	}
	
	@Override
	public int compareTo(AsyncMarker o) {
		if (o == null)
			return 1;
		else
			return Longs.compare(getNewSendingIndex(), o.getNewSendingIndex());
	}
	
	@Override
	public boolean equals(Object other) {
		// Standard equals
		if (other == this)
			return true;
		if (other instanceof AsyncMarker)
			return getNewSendingIndex() == ((AsyncMarker) other).getNewSendingIndex();
		else
			return false;
	}
	
	@Override
	public int hashCode() {
		return Longs.hashCode(getNewSendingIndex());
	}
}
